一直到目前,我們的 component 仍然使用寫死的物件當作資料來源,今天,我們就要來串起我們的前後端,用 HttpClient 取得資料然後再用 component 幫我們把資料顯示在瀏覽器上。
現在這個階段如果我們馬上用 Angular 發 http request 給我們的 .NET API,我們會因為 CORS 而被瀏覽器擋下這個 request,所以在我們開始使用 Angular 的 HttpClient 之前,我必須先來修改一下我們的 .NET API。
我們需要做的事情很簡單,就是在 Startup.cs 裡加入與 CORS 相關的 service 與 middleware,不過這裡要注意一下,app.UseCors("xxx");
需要加在 app.UseRouting();
之後、app.UseEndpoints()
之前。設定 CORS 的更多細節請參考這篇文章。
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(option =>
{
option.AddPolicy("ironmanPolicy", policy =>
{
policy.WithOrigins("http://localhost:4200")
.WithOrigins("https://mydomain.tw")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
}
在 Angular 中,內層的 component 要拿到資料有兩種做法,第一種是我們在 Day26 介紹的屬性繫結,讓外層的 component 把資料傳給內層的 component。另一種方法,是在內層的 component 中直接注入處理資料的 service,讓 service 直接幫我們取得資料。今天,我們就來示範利用注入的方式,讓 http request 幫我們的 component 取得資料。
在我們開始之前,我們先來新增另外一個資料來源
// ironman-list.component.ts
userListFromApi: IronmanUser[] = [];
像 http 這種這麼常用的東西,Angular 當然會提供內建的東西給我們用啦,而這個內建的東西就是 HttpClient 類別,要使用 HttpClient 類別,首先我們得要引用它,到 app.module.ts 中,新增引入 HttpClientModule 的程式碼
// app.module.ts
import { HttpClientModule } from '@angular/common/http';
//...
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule // HttpClientModule 要放在 BrowserModule 後面
],
//...
接著,我們要讓 Angular 幫我們注入 HttpClient 給我們的 component。Angular 的注入與 .NET 很相似,要在建構式中加入所依賴的類別當作參數,然後 Angular 看到建構式有參數,就會在服務容器中找到這個依賴,然後把實體注入給我們 component class。與 .NET 不同的是,在 Angular 中要在建構式使用依賴注入,這個參數必須要有 private 或 public 修飾詞
// ironman-list.component.ts
constructor(private http: HttpClient) { }
注入 HttpClient 之後,我們就可以在 component class 中使用它了
// ironman-list.component.ts
ngOnInit(): void {
this.http.get<IronmanUser[]>(this.apiUrl + '/api/User')
.subscribe(data => {
this.userListFromApi = data;
})
}
我們來看一下這段程式碼,this.http 就是剛剛透過依賴注入取得的 HttpClient 實體,我們呼叫這個實體的 get 泛型方法,從我們的 api 取得型別為「IronmanUser陣列」的資料。
HttpClient 使用觀察者模式設計來處理非同步,使用 http.get()<> 方法會得到一個 Observable<資料型別> 的「可觀察物件」,我們必須訂閱這個可觀察物件,並指定當任務完成時要做什麼後續處理。
觀察者模式就像訂報紙一樣,我們打電話給報社說我們要訂報紙,打完電話我們不會馬上拿到報紙,但是我們知道明天報紙一印好報社就會把報紙送來給我們,我們可以先把柳橙汁放冰箱、培根先買好,隔天報紙一送到我們就能把柳橙汁跟培根拿到餐桌,配報紙享受悠閒的早晨。而在我們上面的例子中,我們透過 HttpClient 跟我們的 .NET API 訂閱一份資料,等到這個資料送達,我們就把這份資料存到 this.userListFromApi 變數,然後 html 頁面再幫我們用內嵌繫結把資料顯示在 table 裡。
填坑
這邊又有一個筆者挖的坑:之前在 DB 裡verified 欄位在 API 使用 bool 型態來接,所以從 API 取回來的 verified 都會是 trule/false,會造成幾個BUG:(1) 編輯的按鈕跑不出來,因為之前用了三個等號(===),true 與 1 不相等。(2) 同樣因為 verified 變成 true/false,ngSwitch 會無法顯示紅色 "BUG" 字樣。(3) POST, PUT request 會變成 400 bad request,因為不符合 API 參數的預期資料型態。
如果要修正這些錯誤,必須把前後端含資料庫的型態統一,可以考慮把 Angular app 裡的 verified 屬性改成 boolean 型態
剛剛我們是直接在 component 裡引入 HttpClient 來替我們打 API,但有些時候我們可能會希望把所有打 user API 的功能寫在一起,然後讓多個 component 共用這個 service,這時候我們就會需要注入我們自己寫的 service,現在我們來稍微修改一下我們的程式,把打 "User" API 的功能寫成一個 service。
首先,先用 ng 指令或點右鍵新增一個 ironman-serviceng g s ironman # g=generate s=service
然後,把 HttpClient 注入給這個 service,再把 打 API 取得使用者資訊的 function 寫在這個 service 裡。
@Injectable({
providedIn: 'root'
})
export class IronmanService {
apiUrl = 'https://mydomain.tw/api';
httpOptions = {
headers: new HttpHeaders({'Content-Type': 'application/json'})
};
constructor(private http: HttpClient) { }
getUserList(): Observable<IronmanUser[]> {
return this.http.get<IronmanUser[]>(`${this.apiUrl}/User`, this.httpOptions);
}
getUserDetail(id: number): Observable<IronmanUser> {
return this.http.get<IronmanUser>(`${this.apiUrl}/User/${id}`, this.httpOptions);
}
addUser(userModel: IronmanUser): Observable<void> {
return this.http.post<void>(`${this.apiUrl}/User`, userModel, this.httpOptions);
}
updateUser(userModel: IronmanUser): Observable<void> {
return this.http.put<void>(
`${this.apiUrl}/User/${userModel.userId}`, userModel, this.httpOptions);
}
deleteUser(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/User/${id}`, this.httpOptions);
}
}
上面的程式碼中,有三個稍微需要留意的地方
service 準備就緒之後,我們再把它加到 app.module.ts 的 provider 陣列裡
// ...
providers: [IronmanService],
// ...
最後,在 component 的建構式注入 IronmanService,然後呼叫 service 的 function 並訂閱,就能從 API 取回資料
constructor(private ironmanService: IronmanService) { }
ngOnInit(): void {
this.ironmanService
.getUserList()
.subscribe(data => {
this.userListFromApi = data;
});
}
最後最後,有一個東西很重要,一定要強調三次
沒有人訂閱,request 就不會發出去
沒有人訂閱,request 就不會發出去
沒有人訂閱,request 就不會發出去
筆者不止一次花 30 分鐘找沒有訂閱造成的 BUG,謹以血淚提醒各位邦友一定要記得訂閱(按讚加分享)。